/**
 * http module
 * 
 * @author strlst <e11907086@student.tuwien.ac.at>
 * @date 2020-01-02
 * @brief contains implementations of extensive client and server methods used
 * to facilitate HTTP exchanges
 */

#include "http.h"

volatile sig_atomic_t exit_requested = 0; /**< used to communicate exit via signals */

/**
 * @brief handles registered signals by requesting exit
 * @param signal integer value for signal that is to be handled
 */
static void handle_signal(int signal) {
    exit_requested = 1;
}

/**
 * @brief helper method that queries whether even to proceed with a request
 * @param req request to check
 * @return integer boolean representing whether request is invalid
 */
static inline int is_request_invalid(request req) {
    return (req.req_port <= 0 ||
        req.http_version == NULL ||
        strcmp(req.http_version, "1.1") != 0 ||
        req.req_method != GET);
}

static const char *method_names[] = {
    "OPTIONS",
    "GET",
    "HEAD",
    "POST",
    "PUT",
    "DELETE",
    "TRACE",
    "CONNECT",
    "OTHER"
}; /**< stores strings associated with values in method enum */

/**
 * @brief takes a request and decomposes it in order to write its contents
 * into a FILE*
 * @details it is assumed that `req` and `sock` were previously
 * initialized and are ready to be used subsequently.
 *
 * is a client method.
 * @param program program name in case 
 * @param req request containing all the necessary components
 * @param sock FILE* to write request to
 */
static void write_request_to_file_p(char *program, request req, FILE *sock) {
    /* query whether to advertise compression or not */
    char *accept_encoding = req.compressed ? "Accept-Encoding: gzip\r\n" : "";

    /* create request string in preparation to write */
    char *req_str = malloc(
        strlen(method_names[req.req_method])
        + strlen(req.req_url.path)
        + strlen(req.http_version)
        + strlen(req.req_url.webserver)
        + strlen(accept_encoding)
        + 38 + 1
    );
    /* error checking */
    if (req_str == NULL)
        die("%s: failed to allocate req_str\n", program, EXIT_FAILURE);

    /* insert variables into template */
    sprintf(
        req_str,
        "%s %s HTTP/%s\r\nHost: %s\r\n%sConnection: close\r\n\r\n",
        method_names[req.req_method],
        req.req_url.path,
        req.http_version,
        req.req_url.webserver,
        accept_encoding
    );

    /* debugging stuff */
    if (DEBUG)
        printf("%s", req_str);

    fputs(req_str, sock);
    fflush(sock);

    free(req_str);
}

/**
 * @brief takes a request and decomposes it in order to serve the file
 * specified in the request
 * @details it is assumed that `sock`, `file` and `ret` were previously
 * initialized and are ready to be used subsequently.
 *
 * contents of sock might not be read fully, don't expect to be able to write
 * to this FILE* later on
 *
 * is a server method.
 * @param program program name to print in the event of errors
 * @param sock FILE* to read request out of
 * @param file FILE* to write request to
 * @param ret pointer to a uint16_t value, used to communicate the result
 * of the parsing process, using return values defined in the def header
 * @return string containing a valid filepath that is ready to be processed
 * with I/O
 */
static char *read_request_to_file_p(char *program, FILE *sock, FILE *file, uint16_t *ret, int *compress) {
    char *req_file = NULL;
    int first_line = 1;
    int header = 1;

    /* parsing variables */
    int max = BUF_SIZE;
    char buf[max];

    /* header fields to be processed */
    char *accept_encoding_p = NULL;

    printf("request:\n");
    while (header && fgets(buf, sizeof(buf), sock) != NULL) {
        fputs(buf, file);
        /* try and decode the first line */
        if (first_line) {
            req_file = parse_request_file(program, buf, max, ret);
            first_line = !first_line;
            continue;
        }

        /* query accept encoding */
        if ((accept_encoding_p = strstr(buf, "Accept-Encoding:")) != NULL) {
            /* skip "Accept-Encoding:" sequence */
            accept_encoding_p += 16;

            char *compress_p = strstr(accept_encoding_p, "gzip");
            *compress = (compress_p != NULL);
            /* don't match sequences like gzipaaaa */
            if (*compress && !(
                compress_p[4] == ';' ||
                compress_p[4] == ',' ||
                compress_p[4] == ' ' ||
                compress_p[4] == '\r' ||
                compress_p[4] == '\n' ||
                compress_p[4] == '\0'
            ))
                *compress = 0;
        }

        /* stop parsing header at this point */
        if (strcmp(buf, "\r\n") == 0)
            header = !header;
    }

    return req_file;
}

/**
 * @brief takes a ret code as defined in the def header and a requested file
 * `req_file` and formulates a response to a previously transmitted http
 * request
 * @details it is assumed that `sock` and `ret` were previously
 * initialized and are ready to be used subsequently.
 *
 * only serves the requested file if:
 * - ret == RQ_OK
 * for this reason `req_file` is allowed to be NULL
 *
 * in all cases the server at least responds by transmitting an http response
 * header
 *
 * is a server method.
 * @param program program name to print in the event of errors
 * @param sock FILE* to read request out of
 * @param req_file string containing a filepath that is ready to be served
 * @param ret uint16_t value, should already be set and is used to decide
 * @param compress boolean integer containing whether to use gzip or not
 * which sort of response will be transmitted
 */
static void write_response_to_file_p(char *program, FILE *sock, char *req_file, uint16_t ret, int compress) {
    char *response = NULL;
    int content_length = 0;
    FILE *req_file_p = NULL;
    FILE *tmp = NULL;

    /* handle 404 */
    if (ret == RQ_OK) {
        /* check if fd is directory */
        DIR* dir = opendir(req_file);
        if (dir)
            closedir(dir);
        /* if it's not a directly, try to open it as a file */
        else if (ENOTDIR == errno)
            req_file_p = fopen(req_file, "r");
        if (req_file_p == NULL)
            ret = RQ_NOT_FOUND;
    }

    /* chose how to construct response header */
    switch (ret) {
        case RQ_OK:
            response = "200 OK";
            break;
        case RQ_NOT_IMPLEMENTED:
            response = "501 Not Implemented";
            break;
        case RQ_NOT_FOUND:
            response = "404 Not Found";
            break;
        case RQ_BAD:
            response = "400 Bad Request";
            break;
        case RQ_PROTOCOL_ERROR:
            response = "400 Bad Request";
            break;
        default:
            response = "400 Bad Request";
            break;
    }

    /* determine raw time */
    time_t rawtime = time(NULL);
    if (rawtime == -1)
        die("%s: failed determining raw time\n", program, EXIT_FAILURE);
    
    /* determine local time */
    struct tm *ptm = localtime(&rawtime);
    if (ptm == NULL)
        die("%s: failed determining local time\n", program, EXIT_FAILURE);

    /* construct date string */
    char date[64];
    strftime(date, 64, "%a, %d %b %Y %T %Z", ptm);

    /* template for the response header */
    char *response_header_template = "HTTP/1.1 %s\r\nDate: %s\r\nContent-Length: %i\r\n%s%sConnection: close\r\n\r\n";

    /* in case we opened a file to serve */
    /* in case of compression size is counted separately */
    if (req_file_p != NULL) {
        if (compress) {
            tmp = tmpfile();
            if (tmp == NULL)
                die("%s: error creating temporary file required for compressing output\n", program, EXIT_FAILURE);

            /* create compressed output */
            int z_ret = Z_OK;
            if ((z_ret = def(req_file_p, tmp, 6)) != Z_OK)
                zerr(program, z_ret);

            /* query length of file */
            fseek(tmp, 0L, SEEK_END);
            content_length = ftell(tmp);
            /* rewind for subsequent read */
            rewind(tmp);
        } else {
            /* query length of file */
            fseek(req_file_p, 0L, SEEK_END);
            content_length = ftell(req_file_p);
            /* rewind for later read */
            rewind(req_file_p);
        }
    }

    char *content_type = NULL;
    if (strstr(req_file, ".html\0") != NULL || strstr(req_file, ".htm") != NULL)
        content_type = "Content-Type: text/html\r\n";
    else if (strstr(req_file, ".css\0") != NULL)
        content_type = "Content-Type: text/css\r\n";
    else if (strstr(req_file, ".js\0") != NULL)
        content_type = "Content-Type: application/javascript\r\n";
    else
        content_type = "";

    char *content_encoding = compress ? "Content-Encoding: gzip\r\n" : "";

    /* debug */
    printf("response:\n");
    printf(       response_header_template, response, date, content_length, content_type, content_encoding);

    /* write to socket */
    fprintf(sock, response_header_template, response, date, content_length, content_type, content_encoding);

    /* in case we opened a file to serve */
    if (req_file_p != NULL) {
        /* dump file to socket */
        char buf[BUF_SIZE];
        int read = 0;
        FILE *in = compress ? tmp : req_file_p;
        // TODO: consider sophisticated error handling
        /* dump file to socket */
        while ((read = fread(buf, 1, sizeof(buf), in)) > 0) {
            // TODO: content_length < sizeof(buf) ? content_length : sizeof(buf)
            int min = content_length < read ? content_length : read;
            fwrite(buf, 1, min, sock);
            content_length -= min;
        }

        /* don't forget to close */
        fclose(req_file_p);
        if (compress)
            fclose(tmp);
    }

    /* don't forget to flush */
    fflush(sock);
}

/**
 * @brief takes to FILE*, one of which contains a as per RFC 7230 (HTTP 1.1)
 * valid http response and in some cases an associated body
 * @details it is assumed that `sock` and `file` were previously
 * initialized and are ready to be used subsequently.
 *
 * response headers are ignored except for the http start-line, which contains
 * important information about the result of the request in the form of a
 * response code.
 *
 * will cause abnormal exit upon encountering:
 * - response code != "200 OK"
 * - invalid header
 *   - http_version != "HTTP/1.1"
 *   - first line does not contain three space separated strings
 *   - header is not terminated with "\r\n\r\n"
 * 
 * is a client method.
 * @param program program name to print in the event of errors
 * @param sock FILE* to read response out of
 * @param file FILE* to output response to
 */
static void read_response_to_file_p(char *program, FILE *sock, FILE *file) {
    /* flags */
    int header = 1;
    int first_line = 1;

    size_t max = BUF_SIZE;
    char buf[max];

    /* header fields to be processed */
    char *content_length_p;
    unsigned int content_length = 0;
    char *content_encoding_p;
    int compressed = 0;

    /* header parsing, line-by-line */
    while (header && fgets(buf, sizeof(buf), sock) != NULL) {
        /* try and decode the first line */
        if (first_line) {
            parse_response_code(program, buf, max);
            first_line = !first_line;
            continue;
        }

        /* query content length */
        if ((content_length_p = strstr(buf, "Content-Length:")) != NULL) {
            content_length_p += 15;

            /* as per documentation, reset errno for strtoul */
            errno = 0;
            content_length = strtoul(content_length_p, NULL, 0);
            /* check for conversion errors */
            if (content_length == 0 && (errno == EINVAL || errno == ERANGE))
                die("%s: error converting content_length: %s\n", program, EXIT_FAILURE);
        }

        /* query content encoding */
        if ((content_encoding_p = strstr(buf, "Content-Encoding:")) != NULL) {
            content_length_p += 17;

            char *compress_p = strstr(content_encoding_p, "gzip");
            compressed = (compress_p != NULL);
            /* don't match sequences like gzipaaaa */
            if (compressed && !(
                compress_p[4] == ';' ||
                compress_p[4] == ',' ||
                compress_p[4] == ' ' ||
                compress_p[4] == '\r' ||
                compress_p[4] == '\n' ||
                compress_p[4] == '\0'
            ))
                compressed = 0;
        }

        if (strcmp(buf, "\r\n") == 0)
            header = !header;
    }

    /* body parsing */
    int read = 0;
    if (compressed) {
        /* create decompressed output */
        int z_ret = Z_OK;
        if ((z_ret = inf(sock, file)) != Z_OK)
            zerr(program, z_ret);
    } else {
        while ((read = fread(buf, 1, sizeof(buf), sock)) > 0) {
            fwrite(buf, 1, read, file);
        }
    }

    /* both the first line and header have to pass */
    if (first_line || header)
        die("%s: response header malformed\n", program, EXIT_FAILURE);
}

void send_request(char *program, request req, FILE *out_file) {
    if (is_request_invalid(req)) {
        fprintf(out_file, "INVALID_REQUEST\n");
        return;
    }

    struct addrinfo hints, *ai;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;

    /* uint16_t is at most 5 characters long */
    char *port_str = malloc(6);
    if (port_str == NULL)
        die("%s: failed to allocate port_str\n", program, EXIT_FAILURE);
    sprintf(port_str, "%u", req.req_port);
    int res = getaddrinfo(req.req_url.webserver, port_str, &hints, &ai);
    if (res != 0)
        die("%s: getaddrinfo error\n", program, EXIT_FAILURE);
    free(port_str);

    /* socket */
    int sock_fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
    if (sock_fd < 0)
        die("%s: error creating socket: %s\n", program, EXIT_FAILURE);

    /* connect */
    if (connect(sock_fd, ai->ai_addr, ai->ai_addrlen) < 0)
        die("%s: error connecting: %s\n", program, EXIT_FAILURE);

    /* prepare for stream I/O */
    FILE *sock = fdopen(sock_fd, "r+");

    /* stream I/O */
    write_request_to_file_p(program, req, sock);
    read_response_to_file_p(program, sock, out_file);

    /* close */
    fclose(sock);

    /* memory cleanup */
    freeaddrinfo(ai);
}

void receive_requests(char *program, uint16_t port, char *doc_root, char *index, int server_compress) {
    /* set up signal handlers for exit */
    struct sigaction sa = { .sa_handler = &handle_signal };
    sigaction(SIGINT,  &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);

    struct addrinfo hints, *ai;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;

    /* uint16_t is at most 5 characters long */
    char *port_str = malloc(6);
    if (port_str == NULL)
        die("%s: failed to allocate port_str\n", program, EXIT_FAILURE);
    sprintf(port_str, "%u", port);
    int res = getaddrinfo(NULL, port_str, &hints, &ai);
    if (res != 0)
        die("%s: getaddrinfo error\n", program, EXIT_FAILURE);
    free(port_str);

    /* socket */
    int sock_fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
    if (sock_fd < 0)
        die("%s: error creating socket: %s\n", program, EXIT_FAILURE);

    /* sock opt */
    int opt_val = 1;
    setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof(opt_val));

    /* bind */
    if (bind(sock_fd, ai->ai_addr, ai->ai_addrlen) < 0)
        die("%s: error binding socket: %s\n", program, EXIT_FAILURE);

    /* listen */
    if (listen(sock_fd, 1) < 0)
        die("%s: error listening socket: %s\n", program, EXIT_FAILURE);

    /* accept in a loop */
    /* in other words, continuously and sequentially process connections */
    int conn_fd;
    uint16_t ret;
    int client_compress;
    while (!exit_requested && (conn_fd = accept(sock_fd, NULL, NULL)) >= 0) {
        /* prepare for stream I/O */
        FILE *sock_in = fdopen(conn_fd, "r");
        if (sock_in == NULL)
            die("%s: error opening file descriptor: %s", program, EXIT_FAILURE);
        int conn_fd_out = dup(conn_fd);
        if (conn_fd_out < 0)
            die("%s: error duplicating connection file descriptor: %s", program, EXIT_FAILURE);
        FILE *sock_out = fdopen(conn_fd_out, "w");
        if (sock_out == NULL)
            die("%s: error opening file descriptor: %s", program, EXIT_FAILURE);

        /* set important values */
        char *file;
        ret = RQ_UNPROCESSED;
        client_compress = 0;

        /* process header sent by connecting party */
        file = read_request_to_file_p(program, sock_in, stdout, &ret, &client_compress);
        fclose(sock_in);

        /* translate request path to filepath */
        if (file != NULL)
            file = translate_requested_file(doc_root, index, file);

        /* debug */
        if (DEBUG)
            printf("server compress: %i, client compress %i\n", server_compress, client_compress);

        /* send response */
        write_response_to_file_p(program, sock_out, file, ret, server_compress && client_compress);
        fclose(sock_out);

        /* free strdup'd resource */
        if (file != NULL)
            free(file);
    }

    /* handle abnormal exit */
    if (!exit_requested)
        die("%s: error accepting connection: %s\n", program, EXIT_FAILURE);

    /* memory cleanup */
    freeaddrinfo(ai);
}